Java程序员面试宝典(三)

61. 目录和文件操作

Java的I/O操作一般需要进行异常检查。
Java对待目录和文件同一使用File来表示,在创建File对象时,并不检查该目录或文件是否存在。若需要,可使用isDirectory()或isFile方法进行判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.io.File;
import java.io.IOException;
public class FileDirTest{
public static void main(String[] args){
File file = new File(“D:/test/a.txt”); //创建文件对象
if (!file.exists()){
try{
file.createNewFile();
} catch(IOException e){
e.printStackTrace();
}
}
File dir = new File(“D:/test”); //创建目录对象
if (dir.isDirectory()){ //如果为目录,则打印里面的所有文件
String[] files = dir.list();
for (String fileName : files){
//用目录名和文件名生成File对象
//separator:系统相关的默认名称分隔符,表示为字符串。
File f= new File(dir.getPath() + File.separator + fileName);
//将文件和目录分类打印
if (f.isFile()) System.out.println(“file:” + f.getName());
else if (f.isDirectory()) System.out.println(“directory:” + f.getName());
}
}
}
}

常用方法:
构造方法:创建File对象,但并不会检查该目录或文件是否已存在;
isDirectory()和isFile()方法:判断是目录还是普通文件;
createNewFile():创建新文件;
list():用于目录,得到目录下的所有文件,类型为字符串数组;
getName():得到文件名,但不包括路径;
delete():删除文件。

62. 写一个复制文件的程序

1) 用a文件(被复制的文件)路径创建一个InputStream对象;
2) 用b文件(新文件)路径创建一个OutputStream对象;
3) 定义固定长度的byte类型数组,用read()方法读取a文件的内容,read()方法返回读取到数组中的字节数,当没有数据了,就返回-1;
4) 用write()方法把数组中的字节写入到b;
5) 关闭输入输出流。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class FileCopy{
public static void main(String[] args){
FileInputStream fis = new FileInputStream(“D:/test/a.txt”);
FileOutputStream fos = new FileOutputStream(“D:/test/b.txt”);
byte[] buff = new byte[256];
int len = 0;
while((len = fis.read(buff)) > 0){
fos.write(buff, 0, len);
}
fis.close();
fos.close();
}
}

其中int read(byte[] b)方法:Reads up to b.length bytes of data from this input stream into an array of bytes. Return total number of bytes read into the buffer, or -1 if there is no more data because the end of the file has been reached.
read()和write()方法的处理目标通常是异构byte数组,即将这些byte写入或读出。

63. 使用随机存取文件RandomAccesFile类

RandomAccessFile类把字节用下标数字来进行定位,通过调用RandomAccessFile的API方法,把指针向前后向后移动,达到随机存取数据的目的。主要方法:
length():得到文件的字节长度;
seek():设置文件指针位置,在该位置发生读取操作;
read()和write():读取和写入一个字节数据。

64. 字节流、字符流

字符流是字节流包装来的,因此在创建BufferedReader和BufferedWriter对象的时候,需要提供一个InputStreamReader或OutputStreamWriter对象。使用read()或readLine()方法得到数据(readLine()直接读取一行数据,并返回String对象),使用write()或print()方法进行字符输出。最后关闭reader或writer。
如FileInputStream -> InputStreamReader -> BufferedReader

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args){
InputStream in = new FileInputStream(“D:/test/a.txt”);
InputStreamReader isr = new InputStreamReader(in, “GBK); //用GBK编码方式读取文本文件内容,不需要关心字节流是如何组装成字符流的
BufferReader br = new BufferReader(isr);
StringBuffer sb = new StringBuffer(); //临时字符内容
String str = null;
while ((br.readLine()) != null){ //循环读取一行数据,并返回String对象
sb.append(br);
}
br.close();
}

65. 序列化(Serialize)和反序列化(Deserialize)

为什么要进行序列化与反序列化:在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用该对象。但是,我们创建出来的这些Java对象都是存在于JVM的堆内存中的。只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止运行,这些对象的状态也就随之而丢失了。但是在真实的应用场景中,我们需要将这些对象持久化下来,并且能够在需要的时候把对象重新读取出来。Java的对象序列化可以帮助我们实现该功能。
对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。对象序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。
在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。
序列化:把内存中的Java对象转换成平台无关的二进制流(字节序列),将对象保存到磁盘中,或允许在网络中直接传输对象;反序列化:读取字节数据,重新组装成Java对象。
所有进行序列化的对象都必须实现Serializable接口,必要时还需要提供序列化ID,即静态常量serialVersionUID。
1) 序列化对象:用一个输出流创建ObjectOutputStream对象,然后调用writeObject()方法;
2) 反序列化:用一个输入流创建ObjectInputStream对象,然后调用readObject()方法。

66. 多线程

每个正在系统上运行的程序都是一个进程,每个进程包含一个或多个线程。进程是程序的一次执行,线程可以理解为进程中执行的一段程序片段。
多线程允许在程序中并发执行多个线程,彼此之间相互独立。多线程共享内存,通过并发执行的方式来提高程序的效率和性能。

67. Runnable接口和Thread类的区别

1)一个类如何成为线程类

(1) 实现java.lang.Runnable接口;
(2) 继承自java.lang.Thread类(实际上Thread类是实现了Runnable接口的)。
Runnable接口和Thread类都需要实现run()方法,当每个线程执行的时候JVM会自动调用这个方法。

2)线程的启动和执行

(1) 线程启动:通过new创建线程对象后,执行start()方法启动一个线程。
(2) 线程执行:run()方法中是线程的主体,也就是线程被启动后将要运行的代码

3)Runnable接口和Thread类的区别

(1) 由于Java只能单继承但能实现多个接口,所以线程类继承了Thread类后就不能继承其它类了,而Runnable接口可以;
(2) 线程类继承Thread类使用线程的方法会更方便一些,因为Thread类提供了一些额外的方法,如获取线程的Id、线程名、线程状态等方法;
(3) 实现Runnable接口的线程类的多线程,可以更方便地访问同一变量,而Thread类则需要内部类来替代,利用的是内部类可以任意访问外部变量这一特性。如

1
2
3
4
5
6
class MyThread implements Runnable{
int index = 0;
public void run(){

}
}
1
2
3
4
5
6
7
8
9
10
11
public MyThread{
int index = 0;
private class InnerClass extends Thread{
public void run(){

}
}
Thread getThread(){
return new InnerClass(); //返回InnerClass的一个匿名对象
}
}

68. 线程同步

多线程操作同一块内存区域的数据可能会造成数据混乱,即线程安全问题。
synchronized工作原理:每个对象都可以有一个线程锁,synchronized可以用任何一个对象的线程锁来锁住一段代码,任何想要进入该段代码的线程必须在解锁后才能执行,否则就进入等待状态。

1) 同步代码块

Java多线程引入了同步监视器,使用同步监视器的通用方法是同步代码块:

1
2
3
4
synchronized(obj){

//同步代码块
}

obj就是同步监视器:阻止两个线程对同一个共享资源进行并发访问
线程开始执行同步代码块之前,必须先获得同步监视器的锁定。
任何时候只能有一个线程可以获得同步监视器的锁定。

2) 同步方法

对于synchronized修饰的实例方法(没有static修饰的方法)无需显示指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。

3) 同步锁(Lock)

Java5提供了功能更强大的同步机制:通过显式定义同步锁对象来实现同步,同步锁由Lock对象充当。
Lock提供了比synchronized代码块和synchronized方法更广泛的锁定操作。Lock是控制多个线程对共享资源进行访问的工具,每次只能有一个线程对Lock对象加锁
Java5提供的两个根接口:Lock、ReadWriteLock,并为Lock提供ReentrantLock(可重入锁)实现类,为ReadWriteLock提供ReentrantReadWrieteLock实现类。Java8新增StampedLock类,大多数场景下可代替ReentrantReadWrieteLock实现类。
可重入即一个线程可以对已被加锁的ReentrantLock再次加锁。
在实现线程安全的控制中,ReentrantLock(可重入锁)对象可以显示地加锁、释放锁。通常代码格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class X{
private final ReentrantLock lock = new ReentrantLock(); //定义锁对象

//定义需要保证线程安全的方法
public void m(){
lock.lock(); //加锁
try{
//需要保证线程安全的代码
} finally{ //使用finally块来保证释放锁,finally用于回收在try块中打开的物理资源
lock.unlock();
}
}
}

同步锁与使用同步方法有点类似,只是使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器。

69. Java线程池(ThreadPool)

系统启动一个新线程的成本是比较高的,因为它涉及操作系统交互。当系统中需要创建大量生存周期短暂的线程时,更应该考虑用线程池。除此之外,使用线程池还可有效控制系统中并发线程的数量,分离了业务代码和线程本身的管理代码。
线程池就是一个或多个线程的集合。线程池有以下几个部分:
(1) 完成任务的一个或多个线程;
(2) 用户调度管理的管理线程;
(3) 要求执行的任务队列。
线程池对应的类为java.util.concurrent.ThreadPoolExecutor,它在构造的时候一般需要提供池大小等参数,常用的构造方法如下:

1
2
3
4
5
6
ThreadPoolExecutor(int coreSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue, //缓冲队列
RejectedExecutionHandler handler);

一个线程任务通过execute(Runnable)方法被添加到线程池,任务就是一个Runnable类型的对象,任务的执行方法就是Runnable类型对象的run()方法。

70. 反射(reflect)的原理

反射是为了动态地调用一个类,动态地调用一个方法,动态地访问一个属性等动态要求而设计的。它的出发点就在于JVM会为每个类创建一个java.lang.Class类的实例,通过该对象可以获取这个类的信息,然后通过使用java.lang.reflect包下的API(application programming interface)达到各种动态需求。大多数的框架,如Struts、Hibernate、Spring都会频繁地使用反射API来完成它们的动态功能。

71. 类初始化的时机

即什么情况下,Java类会被加载到JVM。
1) 创建类的实例。为某个类创建实例的方式包括:new关键字、反射、反序列化;
2) 调用某个类的静态方法(类方法);
3) 访问某个类或接口的静态方法(类方法),或为该静态变量赋值;
4) 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。如使用Class类的静态方法forName()会强制初始化该类,Class.forName(“Person”),如果系统还未初始化Person类,则初始化Person类,并返回Person类对应的java.lang.Class对象;
5) 初始化某个类的子类。当初始化某个类的子类时,该子类的所有父类都会被初始化;
6) 直接使用java.exe命令来运行某个主类。当运行某个主类时,程序会先初始化该主类。

72. 如何得到Class对象

不管通过什么形式,一旦类被加载进JVM,就会为它们创建一个Class类的实例对象。那么如何得到一个类的Class对象呢,有以下几种方法。
1) 使用Class类的forName(String className)方法,其中传入的字符串参数为某个类的全限定包名(有完整包名);(比如使用反射加载数据库驱动程序)
2) 直接调用某个类的class静态属性。如Person.class将会返回Person类对应的Class对象。(这种方法比1)更好,直接调用该类的Class对象,编译阶段就可检查,不用调用方法性能更好)
3) 调用某个对象的getClass()方法。它是Object类中的一个方法。如stu.getClass()。

73. 获取Class对象对应的类信息

获取到Class对象以后,就可通过调用它的一些成员方法来获取它所对应的类的全限定包名(getName())、成员变量(getField())、构造器(getConstructor())、方法(getMethod())、修饰符(getModifier())等信息。

74. 如何操作类的Field(成员变量、属性、字段)

Field提供有关类或接口的单个成员变量信息,以及对它的动态访问权限,反射的变量可能是一个静态变量或实例的变量。
Field对象通过Class类的getField()或getDeclaredField()(获取带指定形参的列表的方法)获取到,Field类处于java.lang.reflect包下。
Field的方法主要分为两类:get***()和set***()。get***()主要为了获取某个对象的变量,如getInt();set***()用于设置值,有两个形参,一个是对象的引用,一个是需要设置的值。

1
2
3
4
5
6
7
8
9
10
11
12
//通过反射比较两个对象的大小
import java.lang.reflect.Field;

class FieldTestClass {
String name;
int age;

public FieldTestClass(String name, int age) {
this.name = name;
this.age = age;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.lang.reflect.Field;

public class FieldTest {
public static void main(String[] args) {
FieldTestClass obj1 = new FieldTestClass("张三", 30);
FieldTestClass obj2 = new FieldTestClass("李四", 40);
System.out.println(compare(obj1, obj2).name + "is bigger.");
}

// 根据age返回成员变量对象
private static FieldTestClass compare(FieldTestClass obj1, FieldTestClass obj2) {
try {
Field field1 = obj1.getClass().getDeclaredField("age");
Field field2 = FieldTestClass.class.getDeclaredField("age");
// 获取两个对象的age值
int val1 = (Integer) field1.get(obj1);
// 或int val1 = field1.getInt(obj1);
int val2 = (Integer) field2.get(obj2);
if (val1 > val2)
return obj1;
else
return obj2;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

75. 如何操作类的Method(方法)

Method类提供关于类或接口中某个方法(以及如何访问该方法)的信息,所反映的方法可能是类方法或实例方法(包括抽象方法在内)。
Method对象通过Class类的getMethod()或getDeclaredMethod()获取到,Method类也处于java.lang.reglect包下。
Method类中最常用的方法是Object inveke()方法,即方法调用。它的第一个参数为Class类对应的实例对象,后面则是不定长Object类型参数列表。

1
2
3
4
5
6
7
8
9
public class MethodTestClass {

public void m1() {
System.out.println("m1 is called...");
}
public void m2() {
System.out.println("m2 is called...");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.lang.reflect.Method;

public class MethodTest {

public static void main(String[] args) {
args = new String[]{"m2"};
String methodName = args[0]; //得到方法名参数
if (methodName != null) {
Class clazz = MethodTestClass.class;
try{
//获取指定方法名的Method对象
Method m = clazz.getDeclaredMethod(methodName);
if (m != null) {
MethodTestClass obj = (MethodTestClass) clazz.newInstance(); //创建一个新对象
m.invoke(obj); //调用obj对象的指定方法
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

76. 如何利用反射实例化一个类

使用反射来创建对象的时候,也需要调用构造器。
(1) 对于默认的无参构造器,用Class类的newInstance()方法即可;

1
2
Class<Student> clazz1 = Student.class; //先获取Student类对应的对象
Student obj1 = clazz1.newInstance(); //创建对象

(2) 对于有参构造器,用java.lang.reflect.Constructor类,它代表了类的构造器方法。为了获取构造器的实例,用Class类的getConstructor()方法。getConstructor()传递的参数就是对应的构造器参数类型的Class实例。得到Constructor对象后,在调用newInstance()方法来创建对象。

1
2
3
4
Student stu = new Student(“张三”, 20);
Class<Student> clazz2 = stu.getClass(); //获取Student类的对象
Constructor<Student> con = clazz2.getConstructor(); //获取Constructor实例
Student obj2 = con.newInstance(); //创建对象

77. 如何使用反射机制访问私有成员

类的私有成员只能被内部的方法所访问,但通过反射,可以访问一个类的所有成员。调用Field类的setAccessible(true)方法。

1
2
3
4
5
private String name;

Class clazz = Student.class; //获取类对应的对象
Field f = clazz.getFieldDeclared(“name”); //获取f对象的name变量
f.setAccessible(true);

78. Java的TCP编程模型

TCP一般用C/S(客户端/服务器端)模式的应用程序,它们都会存在于客户端和服务器端。一般来说,任何的TCP程序,都是先运行服务器端,再运行客户端。
1)对于服务器端:
(1)创建服务器端的Socket,指定一个端口号。java.net.ServerSocket类
ServerSocket ss = new ServerSocket(端口号);
(2)开始监听来自客户端的请求。调用ServerSocket的accept()方法。
aa.accept();
(3)获取输入或输入流。调用Socket的getInputStream()或getOutputStream()方法。
Socekt s = aa.accept();
InputStream is = s.getInputStream(); //获取输入流
OutputStream os = s.getOutputStream(); //获取输出流
(4)调用输入或输出流的read()或write()方法,进行数据的传输。如果是字符流数据,使用BufferedReader或PrintWriter类。
输出:
PrintWriter pw = new PrintWriter(os);
pw.print(内容); //输出内容
pw.flush(); //清空缓存
输入:
InputStreamReader isr = new InputStreamReader(is); //byte(外界)转char(内存)
BufferedReader br = new BufferedReader(isr);
(4)释放资源
pw.close(); //关闭输出流
s.close(); //关闭socket
ss.close(); //关闭server socket
服务器端过策划稿
2)客户端
(1)创建Socket对象,与服务器建立连接。
Socket s = new Socket(“IP地址”, 端口号);
(2)获得输出或输入流。
(3)调用输出或输入流的write()或read()方法,进行数据传输。
(4)释放资源,关闭输出或输入流,socket对象

79. Java的UDP编程模型

UDP一般用于安全性要求不高的点对点传输模式的应用程序,不存在谁是服务器端,谁是客户端。两端的编程方式类似。
(1)创建数据Socket,指定端口号,两端端口号可以不一样。使用java.net.DatagramSocket类。
DatagramSocket s = new DatagramSocket(端口号);
(2)对于发送和接收端,需要byte类型数组用于存储数据,而发送端还需要提供对方的IP地址和端口号。使用DatagramPacket类创建用户数据包对象
接收方:
byte[] buff = new byte[1024];
DatagramPacket dp = new DatagramPacket(buff, 1024); //创建长度小于等于1024(buff长度)的用户数据报对象
发送端:
String str = “数据”; //需要发送的字符串char
DatagtemPacket dp = new
DatagramPacket(str.getBytes(), 0, str.length, InetAddress,getByName(“localhost”), 端口号);
其中byte[] getBytes()将String或charset等转换为byte类型数组。
(3)调用DatagramSocket类的receive()或send()方法,进行数据传输。参数为DatagramPacket对象。
s.receive(dp);
(4)调用DatagramPacket的getData()方法得到byte数组的数据。
dp.getData();
(5)释放资源,关闭DatagramSocket对象
s.close();

80. 创建TCP通信服务器端的多线程模型

(1) 创建ServerSocket对象,指定端口号;
(2) 把accept()方法的返回值作为循环条件,监听客户端请求;
(3) 创建线程类,定义一个Socket类型的成员变量,并定义一个可以为它赋值的构造器;
(4) 在run()方法使用socket变量进行任意操作;
(5) 在主线程的循环体内开启一个线程,并传入accept()方法的返回值。

1
2
3
4
5
ServerSocket ss = new ServerSocket(端口号);
Socket s = null;
while((s==ss.accept()) != null) {

}

此时循环体内需要开启一个线程来处理本次请求,而主线程还要去监听和处理其他请求,必须尽快结束主线程的单次循环。TCP使用连接的输入输出流,如何给线程类的run()方法提供呢?在线程类定义一个Socket类型对象引用,把accept()方法返回的Socket对象通过构造器的方式传递给线程类的对象。

1
2
3
4
5
6
7
8
9
10
11
class MyThread extends Thread{
private Socket s;
public MyThread(Socket s) {
super();
this.s = s; //将accept()方法的返回值传给线程类对象
}
public void run(){
//线程的具体操作内容
}
}
`